# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

"""
This is the OpenAPI validator library.
Validates input using the OpenAPI specification version 3 from
https://github.com/OAI/OpenAPI-Specification (a simplified version, ahem)
"""

import yaml
import json
import functools
import operator
import re


class OpenAPIException(Exception):
    def __init__(self, message):
        self.message = message


# Python type names to JSON type names
py2JSON = {
    "int": "integer",
    "float": "float",
    "str": "string",
    "list": "array",
    "dict": "object",
    "bool": "boolean",
}

mcolors = {
    "PUT": "#fca130",
    "DELETE": "#f93e3e",
    "GET": "#61affe",
    "POST": "#49cc5c",
    "PATCH": "#d5a37e",
}


class OpenAPI:
    def __init__(self, APIFile):
        """ Instantiates an OpenAPI validator given a YAML specification"""
        if APIFile.endswith(".json") or APIFile.endswith(".js"):
            self.API = json.load(open(APIFile))
        else:
            self.API = yaml.load(open(APIFile))

    def validateType(self, field, value, ftype):
        """ Validate a single field value against an expected type """

        # Get type of value, convert to JSON name of type.
        pyType = type(value).__name__
        jsonType = py2JSON[pyType] if pyType in py2JSON else pyType

        # Check if type matches
        if ftype != jsonType:
            raise OpenAPIException(
                "OpenAPI mismatch: Field '%s' was expected to be %s, but was really %s!"
                % (field, ftype, jsonType)
            )

    def validateSchema(self, pdef, formdata, schema=None):
        """ Validate (sub)parameters against OpenAPI specs """

        # allOf: list of schemas to validate against
        if "allOf" in pdef:
            for subdef in pdef["allOf"]:
                self.validateSchema(subdef, formdata)

        where = "JSON body"
        # Symbolic link??
        if "schema" in pdef:
            schema = pdef["schema"]["$ref"]
        if "$ref" in pdef:
            schema = pdef["$ref"]
        if schema:
            # #/foo/bar/baz --> dict['foo']['bar']['baz']
            pdef = functools.reduce(operator.getitem, schema.split("/")[1:], self.API)
            where = "item matching schema %s" % schema

        # Check that all required fields are present
        if "required" in pdef:
            for field in pdef["required"]:
                if not field in formdata:
                    raise OpenAPIException(
                        "OpenAPI mismatch: Missing input field '%s' in %s!"
                        % (field, where)
                    )

        # Now check for valid format of input data
        for field in formdata:
            if "properties" not in pdef or field not in pdef["properties"]:
                raise OpenAPIException(
                    "Unknown input field '%s' in %s!" % (field, where)
                )
            if "type" not in pdef["properties"][field]:
                raise OpenAPIException(
                    "OpenAPI mismatch: Field '%s' was found in api.yaml, but no format was specified in specs!"
                    % field
                )
            ftype = pdef["properties"][field]["type"]
            self.validateType(field, formdata[field], ftype)

            # Validate sub-arrays
            if ftype == "array" and "items" in pdef["properties"][field]:
                for item in formdata[field]:
                    if "$ref" in pdef["properties"][field]["items"]:
                        self.validateSchema(pdef["properties"][field]["items"], item)
                    else:
                        self.validateType(
                            field,
                            formdata[field],
                            pdef["properties"][field]["items"]["type"],
                        )

            # Validate sub-hashes
            if ftype == "hash" and "schema" in pdef["properties"][field]:
                self.validateSchema(pdef["properties"][field], formdata[field])

    def validateParameters(self, defs, formdata):
        #
        pass

    def validate(self, method="GET", path="/foo", formdata=None):
        """ Validate the request method and input data against the OpenAPI specification """

        # Make sure we're not dealing with a dynamic URL.
        # If we find /foo/{key}, we fold that into the form data
        # and process as if it's a json input field for now.
        if not self.API["paths"].get(path):
            for xpath in self.API["paths"]:
                pathRE = re.sub(r"\{(.+?)\}", r"(?P<\1>[^/]+)", xpath)
                m = re.match(pathRE, path)
                if m:
                    for k, v in m.groupdict().items():
                        formdata[k] = v
                    path = xpath
                    break

        if self.API["paths"].get(path):
            defs = self.API["paths"].get(path)
            method = method.lower()
            if method in defs:
                mdefs = defs[method]
                if formdata and "parameters" in mdefs:
                    self.validateParameters(mdefs["parameters"], formdata)
                elif formdata and "requestBody" not in mdefs:
                    raise OpenAPIException(
                        "OpenAPI mismatch: JSON data is now allowed for this request type"
                    )
                elif (
                    formdata
                    and "requestBody" in mdefs
                    and "content" in mdefs["requestBody"]
                ):

                    # SHORTCUT: We only care about JSON input for Kibble! Disregard other types
                    if not "application/json" in mdefs["requestBody"]["content"]:
                        raise OpenAPIException(
                            "OpenAPI mismatch: API endpoint accepts input, but no application/json definitions found in api.yaml!"
                        )
                    jdefs = mdefs["requestBody"]["content"]["application/json"]

                    # Check that required params are here
                    self.validateSchema(jdefs, formdata)

            else:
                raise OpenAPIException(
                    "OpenAPI mismatch: Method %s is not registered for this API"
                    % method
                )
        else:
            raise OpenAPIException("OpenAPI mismatch: Unknown API path '%s'!" % path)

    def dumpExamples(self, pdef, array=False):
        schema = None
        if "schema" in pdef:
            if "type" in pdef["schema"] and pdef["schema"]["type"] == "array":
                array = True
                schema = pdef["schema"]["items"]["$ref"]
            else:
                schema = pdef["schema"]["$ref"]
        if "$ref" in pdef:
            schema = pdef["$ref"]
        if schema:
            # #/foo/bar/baz --> dict['foo']['bar']['baz']
            pdef = functools.reduce(operator.getitem, schema.split("/")[1:], self.API)
        js = {}
        desc = {}
        if "properties" in pdef:
            for k, v in pdef["properties"].items():
                if "description" in v:
                    desc[k] = [v["type"], v["description"]]
                if "example" in v:
                    js[k] = v["example"]
                elif "items" in v:
                    if v["type"] == "array":
                        js[k], foo = self.dumpExamples(v["items"], True)
                    else:
                        js[k], foo = self.dumpExamples(v["items"])
        return [js if not array else [js], desc]

    def toHTML(self):
        """ Blurps out the specs in a pretty HTML blob """
        print(
            """
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
"""
        )
        li = "<h3>Overview:</h3><ul style='font-size: 12px; font-family: Open Sans, sans-serif;'>"
        for path, spec in sorted(self.API["paths"].items()):
            for method, mspec in sorted(spec.items()):
                method = method.upper()
                summary = mspec.get("summary", "No summary available")
                linkname = "%s%s" % (method.lower(), path.replace("/", "-"))
                li += "<li><a href='#%s'>%s %s</a>: %s</li>\n" % (
                    linkname,
                    method,
                    path,
                    summary,
                )
        li += "</ul>"
        print(li)
        for path, spec in sorted(self.API["paths"].items()):
            for method, mspec in sorted(spec.items()):
                method = method.upper()
                summary = mspec.get("summary", "No summary available")
                resp = ""
                inp = ""
                inpvars = ""
                linkname = "%s%s" % (method.lower(), path.replace("/", "-"))
                if "responses" in mspec:
                    for code, cresp in sorted(mspec["responses"].items()):
                        for ctype, pdef in cresp["content"].items():
                            xjs, desc = self.dumpExamples(pdef)
                            js = json.dumps(xjs, indent=4)
                            resp += (
                                "<div style='float: left; width: 90%%;'><pre style='width: 600px;'><b>%s</b>:\n%s</pre>\n</div>\n"
                                % (code, js)
                            )

                if "requestBody" in mspec:
                    for ctype, pdef in mspec["requestBody"]["content"].items():
                        xjs, desc = self.dumpExamples(pdef)
                        if desc:
                            for k, v in desc.items():
                                inpvars += (
                                    "<kbd><b>%s:</b></kbd> (%s) <span style='font-size: 12px; font-family: Open Sans, sans-serif;'>%s</span><br/>\n"
                                    % (k, v[0], v[1])
                                )
                        js = json.dumps(xjs, indent=4)
                        inp += (
                            "<div style='float: left; width: 90%%;'><h4>Input examples:</h4><blockquote><pre style='width: 600px;'><b>%s</b>:\n%s</pre></blockquote>\n</div>"
                            % (ctype, js)
                        )

                if inpvars:
                    inpvars = (
                        "<div style='float: left; width: 90%%;'><blockquote><pre style='width: 600px;'>%s</pre>\n</blockquote></div>"
                        % inpvars
                    )

                print(
                    """
                      <div id="%s" style="margin: 20px; display: flex; box-sizing: border-box; width: 900px; border-radius: 6px; border: 1px solid %s; font-family: sans-serif; background: %s30;">
                        <div style="min-height: 32px;">
                          <!-- method -->

                          <div style="float: left; align-items: center; margin: 4px; border-radius: 5px; text-align: center; padding-top: 4px; height: 20px; width: 100px; color: #FFF; font-weight: bold; background: %s;">%s</div>

                          <!-- path and summary -->
                          <span style="display: flex; padding-top: 6px;"><kbd><strong>%s</strong></kbd></span>
                          <div style="box-sizing: border-box; flex: 1; font-size: 13px; font-family: Open Sans, sans-serif; float: left; padding-top: 6px; margin-left: 20px;">
                          %s</div>
                          <div style="float: left; width: 90%%;display: %s; ">
                            <h4>JSON parameters:</h4>
                            %s
                            <br/>
                            %s
                          </div>
                          <div style="float: left; width: 90%%; ">
                            <h4>Response examples:</h4>
                            <blockquote>%s</blockquote>
                          </div>
                        </div>
                      </div>
                      """
                    % (
                        linkname,
                        mcolors[method],
                        mcolors[method],
                        mcolors[method],
                        method,
                        path,
                        summary,
                        "block" if inp else "none",
                        inpvars,
                        inp,
                        resp,
                    )
                )
                # print("%s %s: %s" % (method.upper(), path, mspec['summary']))
        print("</body></html>")
